// Adapted from https://github.com/ueman/sentry-dart-tools/blob/8e41418c0f2c62dc88292cf32a4f22e79112b744/sentry_flutter_plus/lib/src/widgets/click_tracker.dart
//                                Apache License
//                          Version 2.0, January 2004
//                       http://www.apache.org/licenses/

//  TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

//  1. Definitions.

//     "License" shall mean the terms and conditions for use, reproduction,
//     and distribution as defined by Sections 1 through 9 of this document.

//     "Licensor" shall mean the copyright owner or entity authorized by
//     the copyright owner that is granting the License.

//     "Legal Entity" shall mean the union of the acting entity and all
//     other entities that control, are controlled by, or are under common
//     control with that entity. For the purposes of this definition,
//     "control" means (i) the power, direct or indirect, to cause the
//     direction or management of such entity, whether by contract or
//     otherwise, or (ii) ownership of fifty percent (50%) or more of the
//     outstanding shares, or (iii) beneficial ownership of such entity.

//     "You" (or "Your") shall mean an individual or Legal Entity
//     exercising permissions granted by this License.

//     "Source" form shall mean the preferred form for making modifications,
//     including but not limited to software source code, documentation
//     source, and configuration files.

//     "Object" form shall mean any form resulting from mechanical
//     transformation or translation of a Source form, including but
//     not limited to compiled object code, generated documentation,
//     and conversions to other media types.

//     "Work" shall mean the work of authorship, whether in Source or
//     Object form, made available under the License, as indicated by a
//     copyright notice that is included in or attached to the work
//     (an example is provided in the Appendix below).

//     "Derivative Works" shall mean any work, whether in Source or Object
//     form, that is based on (or derived from) the Work and for which the
//     editorial revisions, annotations, elaborations, or other modifications
//     represent, as a whole, an original work of authorship. For the purposes
//     of this License, Derivative Works shall not include works that remain
//     separable from, or merely link (or bind by name) to the interfaces of,
//     the Work and Derivative Works thereof.

//     "Contribution" shall mean any work of authorship, including
//     the original version of the Work and any modifications or additions
//     to that Work or Derivative Works thereof, that is intentionally
//     submitted to Licensor for inclusion in the Work by the copyright owner
//     or by an individual or Legal Entity authorized to submit on behalf of
//     the copyright owner. For the purposes of this definition, "submitted"
//     means any form of electronic, verbal, or written communication sent
//     to the Licensor or its representatives, including but not limited to
//     communication on electronic mailing lists, source code control systems,
//     and issue tracking systems that are managed by, or on behalf of, the
//     Licensor for the purpose of discussing and improving the Work, but
//     excluding communication that is conspicuously marked or otherwise
//     designated in writing by the copyright owner as "Not a Contribution."

//     "Contributor" shall mean Licensor and any individual or Legal Entity
//     on behalf of whom a Contribution has been received by Licensor and
//     subsequently incorporated within the Work.

//  2. Grant of Copyright License. Subject to the terms and conditions of
//     this License, each Contributor hereby grants to You a perpetual,
//     worldwide, non-exclusive, no-charge, royalty-free, irrevocable
//     copyright license to reproduce, prepare Derivative Works of,
//     publicly display, publicly perform, sublicense, and distribute the
//     Work and such Derivative Works in Source or Object form.

//  3. Grant of Patent License. Subject to the terms and conditions of
//     this License, each Contributor hereby grants to You a perpetual,
//     worldwide, non-exclusive, no-charge, royalty-free, irrevocable
//     (except as stated in this section) patent license to make, have made,
//     use, offer to sell, sell, import, and otherwise transfer the Work,
//     where such license applies only to those patent claims licensable
//     by such Contributor that are necessarily infringed by their
//     Contribution(s) alone or by combination of their Contribution(s)
//     with the Work to which such Contribution(s) was submitted. If You
//     institute patent litigation against any entity (including a
//     cross-claim or counterclaim in a lawsuit) alleging that the Work
//     or a Contribution incorporated within the Work constitutes direct
//     or contributory patent infringement, then any patent licenses
//     granted to You under this License for that Work shall terminate
//     as of the date such litigation is filed.

//  4. Redistribution. You may reproduce and distribute copies of the
//     Work or Derivative Works thereof in any medium, with or without
//     modifications, and in Source or Object form, provided that You
//     meet the following conditions:

//     (a) You must give any other recipients of the Work or
//         Derivative Works a copy of this License; and

//     (b) You must cause any modified files to carry prominent notices
//         stating that You changed the files; and

//     (c) You must retain, in the Source form of any Derivative Works
//         that You distribute, all copyright, patent, trademark, and
//         attribution notices from the Source form of the Work,
//         excluding those notices that do not pertain to any part of
//         the Derivative Works; and

//     (d) If the Work includes a "NOTICE" text file as part of its
//         distribution, then any Derivative Works that You distribute must
//         include a readable copy of the attribution notices contained
//         within such NOTICE file, excluding those notices that do not
//         pertain to any part of the Derivative Works, in at least one
//         of the following places: within a NOTICE text file distributed
//         as part of the Derivative Works; within the Source form or
//         documentation, if provided along with the Derivative Works; or,
//         within a display generated by the Derivative Works, if and
//         wherever such third-party notices normally appear. The contents
//         of the NOTICE file are for informational purposes only and
//         do not modify the License. You may add Your own attribution
//         notices within Derivative Works that You distribute, alongside
//         or as an addendum to the NOTICE text from the Work, provided
//         that such additional attribution notices cannot be construed
//         as modifying the License.

//     You may add Your own copyright statement to Your modifications and
//     may provide additional or different license terms and conditions
//     for use, reproduction, or distribution of Your modifications, or
//     for any such Derivative Works as a whole, provided Your use,
//     reproduction, and distribution of the Work otherwise complies with
//     the conditions stated in this License.

//  5. Submission of Contributions. Unless You explicitly state otherwise,
//     any Contribution intentionally submitted for inclusion in the Work
//     by You to the Licensor shall be under the terms and conditions of
//     this License, without any additional terms or conditions.
//     Notwithstanding the above, nothing herein shall supersede or modify
//     the terms of any separate license agreement you may have executed
//     with Licensor regarding such Contributions.

//  6. Trademarks. This License does not grant permission to use the trade
//     names, trademarks, service marks, or product names of the Licensor,
//     except as required for reasonable and customary use in describing the
//     origin of the Work and reproducing the content of the NOTICE file.

//  7. Disclaimer of Warranty. Unless required by applicable law or
//     agreed to in writing, Licensor provides the Work (and each
//     Contributor provides its Contributions) on an "AS IS" BASIS,
//     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
//     implied, including, without limitation, any warranties or conditions
//     of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
//     PARTICULAR PURPOSE. You are solely responsible for determining the
//     appropriateness of using or redistributing the Work and assume any
//     risks associated with Your exercise of permissions under this License.

//  8. Limitation of Liability. In no event and under no legal theory,
//     whether in tort (including negligence), contract, or otherwise,
//     unless required by applicable law (such as deliberate and grossly
//     negligent acts) or agreed to in writing, shall any Contributor be
//     liable to You for damages, including any direct, indirect, special,
//     incidental, or consequential damages of any character arising as a
//     result of this License or out of the use or inability to use the
//     Work (including but not limited to damages for loss of goodwill,
//     work stoppage, computer failure or malfunction, or any and all
//     other commercial damages or losses), even if such Contributor
//     has been advised of the possibility of such damages.

//  9. Accepting Warranty or Additional Liability. While redistributing
//     the Work or Derivative Works thereof, You may choose to offer,
//     and charge a fee for, acceptance of support, warranty, indemnity,
//     or other liability obligations and/or rights consistent with this
//     License. However, in accepting such obligations, You may act only
//     on Your own behalf and on Your sole responsibility, not on behalf
//     of any other Contributor, and only if You agree to indemnify,
//     defend, and hold each Contributor harmless for any liability
//     incurred by, or claims asserted against, such Contributor by reason
//     of your accepting any such warranty or additional liability.

//  END OF TERMS AND CONDITIONS

//  APPENDIX: How to apply the Apache License to your work.

//     To apply the Apache License to your work, attach the following
//     boilerplate notice, with the fields enclosed by brackets "[]"
//     replaced with your own identifying information. (Don't include
//     the brackets!)  The text should be enclosed in the appropriate
//     comment syntax for the file format. We also recommend that a
//     file or class name and description of purpose be included on the
//     same "printed page" as the copyright notice for easier
//     identification within third-party archives.

//  Copyright 2022 Jonas Uekötter

//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at

//      http://www.apache.org/licenses/LICENSE-2.0

//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:meta/meta.dart';

import '../../sentry_flutter.dart';
import '../widget_utils.dart';
import 'user_interaction_info.dart';

const _tapDeltaArea = 20 * 20;
Element? _clickTrackerElement;

/// Enables the Auto instrumentation for user interaction tracing.
/// It starts a transaction and finishes after the timeout.
/// It adds a breadcrumb as well.
///
/// It's supported by the most common [Widget], for example:
/// [ButtonStyleButton], [MaterialButton], [CupertinoButton], [InkWell],
/// [IconButton], [PopupMenuButton] and [PopupMenuItem].
/// Mostly for onPressed, onTap, and onLongPress events
///
/// Example on how to set up:
/// runApp(SentryWidget(child: App()));
///
/// For transactions, enable it in the [SentryFlutterOptions.enableUserInteractionTracing].
/// The idle timeout can be configured in the [SentryOptions.idleTimeout].
///
/// For breadcrumbs, disable it in the [SentryFlutterOptions.enableUserInteractionBreadcrumbs].
///
/// If you are using the [SentryScreenshotWidget] as well, make sure to add
/// [SentryUserInteractionWidget] as a child of [SentryScreenshotWidget].
class SentryUserInteractionWidget extends StatefulWidget {
  SentryUserInteractionWidget({
    super.key,
    required this.child,
    @internal Hub? hub,
  }) {
    _hub = hub ?? HubAdapter();

    if (_options?.enableUserInteractionTracing ?? false) {
      _options?.sdk.addIntegration('UserInteractionTracing');
    }
  }

  final Widget child;

  late final Hub _hub;

  SentryFlutterOptions? get _options =>
      // ignore: invalid_use_of_internal_member
      _hub.options is SentryFlutterOptions
          // ignore: invalid_use_of_internal_member
          ? _hub.options as SentryFlutterOptions
          : null;

  @override
  StatefulElement createElement() {
    final element = super.createElement();
    _clickTrackerElement = element;
    return element;
  }

  @override
  _SentryUserInteractionWidgetState createState() =>
      _SentryUserInteractionWidgetState();
}

class _SentryUserInteractionWidgetState
    extends State<SentryUserInteractionWidget> {
  int? _lastPointerId;
  Offset? _lastPointerDownLocation;
  UserInteractionInfo? _lastTappedWidget;
  ISentrySpan? _activeTransaction;

  Hub get _hub => widget._hub;

  SentryFlutterOptions? get _options => widget._options;

  @override
  Widget build(BuildContext context) {
    if ((_options?.enableUserInteractionTracing ?? true) ||
        (_options?.enableUserInteractionBreadcrumbs ?? true)) {
      return Listener(
        behavior: HitTestBehavior.translucent,
        onPointerDown: _onPointerDown,
        onPointerUp: _onPointerUp,
        child: widget.child,
      );
    }
    return widget.child;
  }

  void _onPointerDown(PointerDownEvent event) {
    try {
      _lastPointerId = event.pointer;
      _lastPointerDownLocation = event.localPosition;
    } catch (exception, stacktrace) {
      _options?.logger(
        SentryLevel.error,
        'Error while handling pointer-down event $event in $SentryUserInteractionWidget',
        exception: exception,
        stackTrace: stacktrace,
      );
      // ignore: invalid_use_of_internal_member
      if (_options?.automatedTestMode ?? false) {
        rethrow;
      }
    }
  }

  void _onPointerUp(PointerUpEvent event) {
    try {
      // Figure out if something was tapped
      final location = _lastPointerDownLocation;
      if (location == null || event.pointer != _lastPointerId) {
        return;
      }
      final delta = Offset(
        location.dx - event.localPosition.dx,
        location.dy - event.localPosition.dy,
      );

      if (delta.distanceSquared < _tapDeltaArea) {
        // Widget was tapped
        _onTappedAt(event.localPosition);
      }
    } catch (exception, stacktrace) {
      _options?.logger(
        SentryLevel.error,
        'Error while handling pointer-up event $event in $SentryUserInteractionWidget',
        exception: exception,
        stackTrace: stacktrace,
      );
      // ignore: invalid_use_of_internal_member
      if (_options?.automatedTestMode ?? false) {
        rethrow;
      }
    }
  }

  void _onTappedAt(Offset position) {
    final tapInfo = _getElementAt(position);
    if (tapInfo == null) {
      return;
    }

    final widgetKey = WidgetUtils.toStringValue(tapInfo.element.widget.key);
    _createBreadcrumbOnTap(tapInfo, widgetKey);
    _startTransactionOnTap(tapInfo, widgetKey);
  }

  void _createBreadcrumbOnTap(UserInteractionInfo info, String? widgetKey) {
    if (!(_options?.enableUserInteractionBreadcrumbs ?? false)) {
      return;
    }

    final label = _getLabelRecursively(info.element);
    final data = {
      'path': _getTouchPath(info.element),
      if (label != null) 'label': label
    };

    final crumb = Breadcrumb.userInteraction(
      subCategory: 'click',
      viewId: widgetKey,
      viewClass: info.type, // to avoid minification
      data: data,
    );
    final hint = Hint.withMap({TypeCheckHint.widget: info.element.widget});
    _hub.addBreadcrumb(crumb, hint: hint);
  }

  List<Map<String, String?>> _getTouchPath(Element element) {
    final path = <Map<String, String?>>[];

    bool addToPath(Element element) {
      // Break at the boundary (i.e. this [SentryUserInteractionWidget]).
      if (element.widget == widget) {
        return false;
      }

      final widgetName = element.widget.runtimeType.toString();
      if (!widgetName.startsWith('_')) {
        final info = {
          'name': WidgetUtils.toStringValue(element.widget.key),
          'element': _getElementType(element) ?? widgetName,
          'label': _getLabel(element, true),
        }..removeWhere((key, value) => value == null);
        if (info.isNotEmpty) {
          path.add(info);
        }
      }

      return path.length < 10;
    }

    if (addToPath(element)) {
      element.visitAncestorElements(addToPath);
    }

    return path;
  }

  void _startTransactionOnTap(UserInteractionInfo info, String? widgetKey) {
    if (widgetKey == null ||
        !(_options?.isTracingEnabled() ?? false) ||
        !(_options?.enableUserInteractionTracing ?? false)) {
      return;
    }

    final element = info.element;
    // getting the name of the screen using ModalRoute.of(context).settings.name
    // is expensive, so we expect that the keys are unique across the app
    final transactionContext = SentryTransactionContext(
      widgetKey,
      'ui.action.click',
      transactionNameSource: SentryTransactionNameSource.component,
    );

    final activeTransaction = _activeTransaction;
    final lastElement = _lastTappedWidget?.element;
    if (activeTransaction != null) {
      if (_isElementMounted(lastElement) &&
          _isElementMounted(element) &&
          lastElement?.widget == element.widget &&
          !activeTransaction.finished) {
        // ignore: invalid_use_of_internal_member
        activeTransaction.scheduleFinish();
        return;
      } else {
        activeTransaction.finish();
        _hub.configureScope((scope) {
          if (scope.span == activeTransaction) {
            scope.span = null;
          }
        });
        _activeTransaction = null;
        _lastTappedWidget = null;
      }
    }

    _lastTappedWidget = info;

    bool hasRunningTransaction = false;
    _hub.configureScope((scope) {
      if (scope.span != null) {
        hasRunningTransaction = true;
      }
    });

    if (hasRunningTransaction) {
      return;
    }

    // TODO: mobile vitals
    _activeTransaction = _hub.startTransactionWithContext(
      transactionContext,
      waitForChildren: true,
      autoFinishAfter: _options?.idleTimeout,
      trimEnd: true,
    );

    // if _enableAutoTransactions is enabled but there's no traces sample rate
    if (_activeTransaction is NoOpSentrySpan) {
      return;
    }

    _hub.configureScope((scope) {
      scope.span ??= _activeTransaction;
    });
  }

  String? _getLabel(Element element, bool allowText) {
    String? label;

    if (_options?.sendDefaultPii ?? false) {
      final widget = element.widget;
      if (allowText && widget is Text) {
        label = widget.data;
      } else if (widget is Semantics) {
        label = widget.properties.label;
      } else if (widget is Icon) {
        label = widget.semanticLabel;
      } else if (widget is Tooltip) {
        label = widget.message;
      }

      if (label?.isEmpty ?? true) {
        label = null;
      }
    }

    return label;
  }

  String? _getLabelRecursively(Element element) {
    String? label;

    if (_options?.sendDefaultPii ?? false) {
      final widget = element.widget;
      final allowText = widget is ButtonStyleButton ||
          widget is MaterialButton ||
          widget is CupertinoButton;

      // traverse tree to find a suiting element
      void descriptionFinder(Element element) {
        label ??= _getLabel(element, allowText);
        if (label == null) {
          element.visitChildren(descriptionFinder);
        }
      }

      descriptionFinder(element);
    }

    return label;
  }

  UserInteractionInfo? _getElementAt(Offset position) {
    // WidgetsBinding.instance.renderViewElement does not work, so using
    // the element from createElement
    final rootElement = _clickTrackerElement;
    if (rootElement == null || rootElement.widget != widget) {
      return null;
    }

    UserInteractionInfo? tappedWidget;

    void elementFinder(Element element) {
      if (tappedWidget != null) {
        // element was found
        return;
      }

      final renderObject = element.renderObject;
      if (renderObject == null) {
        return;
      }
      var hitFound = true;
      if (renderObject is RenderPointerListener) {
        final hitResult = BoxHitTestResult();

        // Returns false if the hit can continue to other objects below this one.
        hitFound = renderObject.hitTest(hitResult, position: position);
      }

      final transform = renderObject.getTransformTo(rootElement.renderObject);
      final paintBounds =
          MatrixUtils.transformRect(transform, renderObject.paintBounds);

      if (!paintBounds.contains(position)) {
        return;
      }

      final type = _getElementType(element);
      if (type != null) {
        tappedWidget = UserInteractionInfo(
          element: element,
          type: type,
        );
      }

      if (tappedWidget == null || !hitFound) {
        element.visitChildElements(elementFinder);
      }
    }

    rootElement.visitChildElements(elementFinder);

    return tappedWidget;
  }

  String? _getElementType(Element element) {
    final widget = element.widget;
    // Used by ElevatedButton, TextButton, OutlinedButton.
    if (widget is ButtonStyleButton) {
      if (widget.enabled) {
        return 'ButtonStyleButton';
      }
    } else if (widget is MaterialButton) {
      if (widget.enabled) {
        return 'MaterialButton';
      }
    } else if (widget is CupertinoButton) {
      if (widget.enabled) {
        return 'CupertinoButton';
      }
    } else if (widget is InkWell) {
      if (widget.onTap != null) {
        return 'InkWell';
      }
    } else if (widget is IconButton) {
      if (widget.onPressed != null) {
        return 'IconButton';
      }
    } else if (widget is PopupMenuButton) {
      if (widget.enabled) {
        return 'PopupMenuButton';
      }
    } else if (widget is PopupMenuItem) {
      if (widget.enabled) {
        return 'PopupMenuItem';
      }
    }

    return null;
  }

  bool _isElementMounted(Element? element) {
    if (element == null) {
      return false;
    }
    try {
      // ignore: return_of_invalid_type
      return (element as dynamic).mounted;
    } on NoSuchMethodError catch (_) {
      // mounted checks if the widget is not null.

      try {
        // Flutter 3.0.0 does `_widget!` and if `_widget` is null it throws.

        // ignore: unnecessary_null_comparison
        return element.widget != null;
      } catch (_) {
        // if it throws, the `_widget` is null and not mounted.
        return false;
      }
    }
  }
}
